﻿using System;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.WindowsAzure.StorageClient;
using System.Data.Services.Client;
using AntsCode.StorageClientExtensions.Helpers;
using System.Threading;
using System.Reflection;
using System.Collections.Generic;

namespace AntsCode.StorageClientExtensions.QueryHandlers
{
    /// <summary>
    /// Handles a Join query
    /// </summary>
    internal class JoinQueryHandler : QueryHandler
    {
        /// <summary>
        /// The maximum number of query parameters permitted by Table Storage
        /// </summary>
        private const int maxTableServiceQueryParams = 15;

        public JoinQueryHandler(TableServiceContext context, QueryExecutor queryExecutor, Expression e, MethodCallExpression m, IQueryable data, ContinuationToken continuationToken)
            : base(data, continuationToken)
        {
            // Get the joins for this group
            List<JoinItem> joins = this.GetJoinsForGroup(e, m, null, null);

            // Has this group already been processed?
            if (joins != null)
            {
                // No, so sort the joins into batches
                List<List<JoinBatchItem>> batches = this.BatchJoins(joins);

                // Execute each batch
                foreach (List<JoinBatchItem> batch in batches)
                {
                    data = this.ExecuteBatch(context, queryExecutor, e, batch, data);
                }
            }

            // Set the data property with joined data
            this.Data = data;
        }

        /// <summary>
        /// If the supplied MethodCallExpression is the first join method in a series of join methods, the series will be returned.
        /// If the supplied method is not the first in the series then null will be returned.
        /// </summary>
        /// <param name="expression">The parent expression to search within.</param>
        /// <param name="method">The join method to search for.</param>
        /// <param name="joins">The default join collection (pass null).</param>
        /// <returns>A list of join methods, or null.</returns>
        private List<JoinItem> GetJoinsForGroup(Expression expression, MethodCallExpression method, List<JoinItem> joins, MethodCallExpression selectMany)
        {
            if (expression.NodeType == ExpressionType.Call)
            {
                MethodCallExpression m = (MethodCallExpression)expression;
                string methodName = m.Method.Name;

                // Is this method a join method?
                if (methodName == "Join" || methodName == "GroupJoin")
                {
                    // Is this the method we are looking for?
                    if (m == method)
                    {
                        // Yes, so initialise the join list
                        joins = new List<JoinItem>();
                    }

                    // Add the join if the list has been initialised
                    if (joins != null)
                    {
                        // Recurse
                        joins = GetJoinsForGroup(m.Arguments[0], method, joins, null);

                        // Add the join
                        joins.Add(new JoinItem() { JoinExpression = m, SelectManyExpression = selectMany });
                    }
                }
                else if (methodName == "SelectMany")
                {
                    // This method corresponds to the next GroupJoin method to form an outer join
                    joins = GetJoinsForGroup(m.Arguments[0], method, joins, m);
                }
                else
                {
                    // Method is not a join method, are we at the end of a group?
                    if (joins == null)
                    {
                        // No, so recurse
                        joins = GetJoinsForGroup(m.Arguments[0], method, joins, null);
                    }
                }
            }

            return joins;
        }

        /// <summary>
        /// Sorts a list of joins into batches based on their source data dependency, or if they are
        /// asyncronous methods.
        /// </summary>
        /// <param name="joins">The list of joins to sort.</param>
        /// <returns>A list of batches of MethodCallExpressions.</returns>
        private List<List<JoinBatchItem>> BatchJoins(List<JoinItem> joins)
        {
            List<List<JoinBatchItem>> batches = new List<List<JoinBatchItem>>();
            List<JoinBatchItem> currentBatch = null;
            JoinBatchItem currentBatchItem;
            UnaryExpression outerUnary;
            UnaryExpression innerUnary;
            UnaryExpression resultUnary;
            IXTableServiceQuery outerQuery;
            IXTableServiceQuery innerQuery;
            Expression parentOuterKey = null;

            foreach (JoinItem join in joins)
            {
                outerUnary = (UnaryExpression)join.JoinExpression.Arguments[2];
                innerUnary = (UnaryExpression)join.JoinExpression.Arguments[3];
                resultUnary = (UnaryExpression)join.JoinExpression.Arguments[4];
                outerQuery = GetValueFromConstant<IXTableServiceQuery>(join.JoinExpression.Arguments[0]);
                innerQuery = GetValueFromConstant<IXTableServiceQuery>(join.JoinExpression.Arguments[1]);

                // Look for an existing batch based on the outer key type
                currentBatch = this.FindBatchFromOuterKeyType((Expression)outerUnary.Operand, batches);

                if (currentBatch == null)
                {
                    // Create a new batch
                    currentBatch = new List<JoinBatchItem>();
                    batches.Add(currentBatch);
                }

                // Get all information required to retrieve data & perform joins
                currentBatchItem = new JoinBatchItem()
                {
                    JoinExpression = join.JoinExpression,
                    SelectManyExpression = join.SelectManyExpression,
                    OuterType = TypeSystem.GetElementType(join.JoinExpression.Arguments[0].Type),
                    InnerType = TypeSystem.GetElementType(join.JoinExpression.Arguments[1].Type),
                    OuterKey = (Expression)outerUnary.Operand,
                    InnerKey = (Expression)innerUnary.Operand,
                    ResultSelector = (Expression)resultUnary.Operand,
                    OuterKeyType = outerUnary.Operand.Type.GetGenericArguments()[1],
                    InnerKeyType = innerUnary.Operand.Type.GetGenericArguments()[1],
                    ResultType = resultUnary.Operand.Type.GetGenericArguments()[2],
                    OuterEntitySet = outerQuery == null ? null : outerQuery.EntitySetName,
                    InnerEntitySet = innerQuery == null ? null : innerQuery.EntitySetName
                };

                // Is this an asyncronous join?
                if (this.IsAsyncJoin(currentBatchItem.OuterKeyType))
                {
                    // Yes, so always add to first batch as there is no dependency
                    batches[0].Add(currentBatchItem);
                }
                else
                {
                    // Add the join to the current batch
                    currentBatch.Add(currentBatchItem);
                }

                // Update the parent outer key
                parentOuterKey = outerUnary.Operand;
            }

            return batches;
        }

        private List<JoinBatchItem> FindBatchFromOuterKeyType(Expression outerKey, List<List<JoinBatchItem>> batches)
        {
            List<JoinBatchItem> returnBatch = null;

            foreach (List<JoinBatchItem> batch in batches)
            {
                if (this.AreOuterKeyDependenciesMet(outerKey, batch.First().OuterKey))
                {
                    returnBatch = batch;
                    break;
                }
            }

            return returnBatch;
        }

        private bool AreOuterKeyDependenciesMet(Expression outerKey, Expression parentOuterKey)
        {
            bool dependenciesMet;

            if (parentOuterKey == null)
            {
                dependenciesMet = false;
            }
            else
            {
                var outerKeyArgs = this.GetMemberExpressions(GetArgumentsFromNewExpression(outerKey), new List<MemberExpression>());
                var parentOuterKeyArgs = this.GetMemberExpressions(GetArgumentsFromNewExpression(parentOuterKey), new List<MemberExpression>());

                dependenciesMet = true;

                // Check each argument
                foreach (MemberExpression arg in outerKeyArgs)
                {
                    // Is the type of this argument included in the parent key?
                    if (parentOuterKeyArgs.FirstOrDefault(a => a.Member.DeclaringType == arg.Member.DeclaringType) == null)
                    {
                        // No, so not all depencies are met
                        dependenciesMet = false;
                        break;
                    }
                }
            }

            return dependenciesMet;
        }

        private IEnumerable<Expression> GetArgumentsFromNewExpression(Expression source)
        {
            LambdaExpression lambdaExp = (LambdaExpression)source;
            NewExpression newExp = (NewExpression)lambdaExp.Body;

            return newExp.Arguments;
        }

        private List<MemberExpression> GetMemberExpressions(IEnumerable<Expression> arguments, List<MemberExpression> memberExpressions)
        {
            foreach (Expression argument in arguments)
            {
                // Is this argument a member expression?
                if (argument.GetType() == typeof(MemberExpression))
                {
                    memberExpressions.Add((MemberExpression)argument);
                }
                else if (argument.GetType() == typeof(MethodCallExpression))
                {
                    // Recurse to look for member expressions as method parameters
                    MethodCallExpression methodCall = (MethodCallExpression)argument;
                    memberExpressions = GetMemberExpressions(methodCall.Arguments, memberExpressions);
                }
                else if (argument.GetType() == typeof(ConditionalExpression))
                {
                    // Recurse to look for member expressions in condition parts
                    ConditionalExpression conditional = (ConditionalExpression)argument;
                    List<Expression> expressions = new List<Expression>();
                    expressions.Add(conditional.IfFalse);
                    expressions.Add(conditional.IfTrue);
                    expressions.Add(conditional.Test);

                    memberExpressions = GetMemberExpressions(expressions, memberExpressions);
                }
            }

            return memberExpressions;
        }

        /// <summary>
        /// Executes a batch of join expressions.
        /// </summary>
        /// <param name="e">The parent expression.</param>
        /// <param name="batch">The batch to execute.</param>
        /// <param name="outerData">Any existing outer data.</param>
        /// <param name="context">The TableServiceContext.</param>
        /// <returns>An IQueryable result set of joined data.</returns>
        private IQueryable ExecuteBatch(TableServiceContext context, QueryExecutor queryExecutor, Expression e, List<JoinBatchItem> batch, IQueryable outerData)
        {
            bool firstBatch = true;

            // The outer query should use any existing continuation token
            ContinuationToken outerContinuationToken = this.ContinuationToken;

            // Start retrieving all data that has no dependency on the outer data
            queryExecutor.IncrementQueryBatchNo();

            foreach (JoinBatchItem batchItem in batch)
            {
                // If there is no outer data initially, then get it based on the source type of the first join
                if (outerData == null && firstBatch)
                {
                    IGetOuterData getOuterJoinData = (IGetOuterData)Activator.CreateInstance(typeof(GetOuterData<>).MakeGenericType(batchItem.OuterType));
                    batchItem.OuterJoinDelegate = getOuterJoinData.GetData;

                    // Start query execution in a new thread
                    batchItem.OuterAsyncResult = batchItem.OuterJoinDelegate.BeginInvoke(e, context, queryExecutor, batchItem.OuterEntitySet, ref outerContinuationToken, true, null, null);
                }

                // Start getting any inner data that is flagged as asyncronous
                if (this.IsAsyncJoin(batchItem.OuterKeyType))
                {
                    IGetInnerData getInnerJoinData = (IGetInnerData)Activator.CreateInstance(typeof(GetInnerData<,,,>).MakeGenericType
                            (batchItem.OuterType, batchItem.InnerType, batchItem.OuterKeyType, batchItem.OuterType));

                    batchItem.InnerJoinDelegate = getInnerJoinData.GetAllData;

                    // Start query execution in a new thread
                    batchItem.InnerAsyncResult = batchItem.InnerJoinDelegate.BeginInvoke(context, queryExecutor, batchItem.InnerEntitySet, batchItem.OuterKey, batchItem.InnerKey, null, null);
                }

                firstBatch = false;
            }

            // Start retrieving data that is dependent on the outer data
            firstBatch = true;

            foreach (JoinBatchItem batchItem in batch)
            {
                // Get the initial outer data if required
                if (outerData == null && firstBatch)
                {
                    outerData = batchItem.OuterJoinDelegate.EndInvoke(ref outerContinuationToken, batchItem.OuterAsyncResult);

                    // Update the class continuation token based on the outer query result
                    this.ContinuationToken = outerContinuationToken;

                    queryExecutor.IncrementQueryBatchNo();
                }

                // Now that we have the outer data, get any inner data that is not flagged as asyncronous
                // (i.e. dependent on the outer data)
                if (!this.IsAsyncJoin(batchItem.OuterKeyType))
                {
                    IGetInnerData getInnerJoinDataUnique = (IGetInnerData)Activator.CreateInstance(typeof(GetInnerData<,,,>).MakeGenericType
                        (outerData.ElementType, batchItem.InnerType, batchItem.OuterKeyType, batchItem.OuterType));
                    
                    batchItem.InnerJoinUniqueDelegate = getInnerJoinDataUnique.GetDataFromUniqueOuter;

                    // Start query execution in a new thread
                    batchItem.InnerAsyncResult = batchItem.InnerJoinUniqueDelegate.BeginInvoke
                        (context, queryExecutor, batchItem.InnerEntitySet, outerData, batchItem.OuterKey, batchItem.InnerKey, null, null);
                }

                firstBatch = false;
            }

            // Get all inner data and perform joins
            foreach (JoinBatchItem batchItem in batch)
            {
                if (batchItem.InnerJoinDelegate != null)
                {
                    batchItem.InnerData = batchItem.InnerJoinDelegate.EndInvoke(batchItem.InnerAsyncResult);
                }
                else if (batchItem.InnerJoinUniqueDelegate != null)
                {
                    batchItem.InnerData = batchItem.InnerJoinUniqueDelegate.EndInvoke(batchItem.InnerAsyncResult);
                }

                IJoinData joinData = (IJoinData)Activator.CreateInstance(typeof(JoinData<,,,>).MakeGenericType
                    (batchItem.OuterType, batchItem.InnerType, batchItem.OuterKeyType, batchItem.ResultType));

                outerData = joinData.Join(batchItem.JoinExpression.Method.Name, outerData, batchItem.InnerData, 
                    batchItem.OuterKey, batchItem.InnerKey, batchItem.ResultSelector, batchItem.SelectManyExpression);
            }

            return outerData;
        }

        private bool IsAsyncJoin(Type outerKeyType)
        {
            if (outerKeyType.GetProperty("Async") != null)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private delegate IQueryable GetOuterDataDelegate(Expression e, TableServiceContext context, QueryExecutor queryExecutor, string entitySetName, ref ContinuationToken continuationToken, bool addWhere);

        private interface IGetOuterData
        {
            IQueryable GetData(Expression e, TableServiceContext context, QueryExecutor queryExecutor, string entitySetName, ref ContinuationToken continuationToken, bool addWhere);
        }

        private class GetOuterData<T> : IGetOuterData
        {
            public IQueryable GetData(Expression e, TableServiceContext context, QueryExecutor queryExecutor, string entitySetName, ref ContinuationToken continuationToken, bool addWhere)
            {
                var query = (DataServiceQuery<T>)from x in context.CreateQuery<T>(entitySetName)
                                                 select x;

                if (addWhere)
                {
                    // Suffix any where criteria from the parent expression
                    query = SuffixQuery.SuffixWhere(e, query);
                    query = SuffixQuery.SuffixExtras(e, query);
                }

                return queryExecutor.ExecuteAll(query, ref continuationToken);
            }
        }

        private delegate IQueryable GetInnerDataFromUniqueDelegate(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, IQueryable outerData, Expression outerKey, Expression innerKey);
        private delegate IQueryable GetAllInnerDataDelegate(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, Expression outerKey, Expression innerKey);

        private interface IGetInnerData
        {
            IQueryable GetDataFromUniqueOuter(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, IQueryable outerData, Expression outerKey, Expression innerKey);
            IQueryable GetAllData(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, Expression outerKey, Expression innerKey);
        }

        private class GetInnerData<T, U, V, W> : IGetInnerData
        {
            private delegate IQueryable<U> QueryExecutorDelegate(DataServiceQuery<U> query, ref ContinuationToken continuationToken);

            public IQueryable GetDataFromUniqueOuter(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, IQueryable outerData, Expression outerKey, Expression innerKey)
            {
                var outerDataGeneric = (IQueryable<T>)outerData;
                var innerKeyGeneric = (Expression<Func<U, V>>)innerKey;

                Expression<Func<T, V>> outerKeyGeneric;

                if (typeof(T) != typeof(W))
                {
                    // The type of the outer data is different to the original type of the join,
                    // so we need to convert the outer key expression to the outer data type
                    ExpressionConverter<V, T> expConv = new ExpressionConverter<V, T>();
                    outerKeyGeneric = (Expression<Func<T, V>>)expConv.ConvertExpression(outerKey);
                }
                else
                {
                    outerKeyGeneric = (Expression<Func<T, V>>)outerKey;
                }

                // Get a list of unique keys for retrieving inner data
                var uniqueKeys = outerDataGeneric.GroupBy<T, V>(outerKeyGeneric);

                // Retrieve inner data for each key asynchronously, grouped into max number of parameters 
                // (maxTableServiceFilterParams is the max parameter count in table storage)
                BinaryExpression queryExp = null;
                BinaryExpression keyExp;
                BinaryExpression partitionWhere = null;
                List<InnerQuery> innerQueries = new List<InnerQuery>();
                ContinuationToken continuationToken = null;
                int paramCount = 0;
                int paramTotal = 0;

                foreach (IGrouping<V, T> keyGroup in uniqueKeys)
                {
                    // Get the filter expression for this key
                    keyExp = this.BuildInnerWhere(innerKeyGeneric.Body, keyGroup.Key, out paramCount, ref partitionWhere);

                    if (keyExp != null)
                    {
                        paramTotal += paramCount;

                        // Have we exceeded the max parameters for this query?
                        if (paramTotal > maxTableServiceQueryParams)
                        {
                            // Begin executing the current query
                            innerQueries.Add(this.BeginExecuteWhere(context, queryExecutor, innerEntitySetName, queryExp, partitionWhere));

                            // Create a new query
                            queryExp = keyExp;
                            paramTotal = paramCount;
                            partitionWhere = null;
                        }
                        else if (queryExp == null)
                        {
                            // This the first expression
                            queryExp = keyExp;
                        }
                        else
                        {
                            // OR this expression with existing expressions
                            queryExp = Expression.Or(queryExp, keyExp);
                        }
                    }
                }

                // Begin executing the remaining query
                if (uniqueKeys.Count() > 0 && !(queryExp == null && partitionWhere == null))
                {
                    innerQueries.Add(this.BeginExecuteWhere(context, queryExecutor, innerEntitySetName, queryExp, partitionWhere));
                }

                // Get the data for each inner query
                List<U> results = new List<U>();

                foreach(InnerQuery innerQuery in innerQueries)
                {
                    results.AddRange(innerQuery.Delegate.EndInvoke(ref continuationToken, innerQuery.Result));
                }

                return results.AsQueryable();
            }

            public IQueryable GetAllData(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, Expression outerKey, Expression innerKey)
            {
                var outerKeyGeneric = (Expression<Func<T, V>>)outerKey;
                var innerKeyGeneric = (Expression<Func<U, V>>)innerKey;
                BinaryExpression partitionWhere = null;
                int paramCount = 0;
                
                // Get the filter expression
                BinaryExpression where = this.BuildInnerWhere(innerKeyGeneric.Body, outerKeyGeneric.Body, out paramCount, ref partitionWhere);

                // Append the partition key to the where if it exists
                if (partitionWhere != null)
                {
                    if (where != null)
                    {
                        where = Expression.And(partitionWhere, where);
                    }
                    else
                    {
                        where = partitionWhere;
                    }
                }

                // Execute the query
                DataServiceQuery<U> dataServiceQuery = (DataServiceQuery<U>)context.CreateQuery<U>(innerEntitySetName).Where<U>(BinaryToLambda<U>(where));
                ContinuationToken continuationToken = null;

                return queryExecutor.ExecuteAll(dataServiceQuery, ref continuationToken);
            }

            private InnerQuery BeginExecuteWhere(TableServiceContext context, QueryExecutor queryExecutor, string innerEntitySetName, BinaryExpression where, BinaryExpression partitionWhere)
            {
                // Append the partition key to the where if it exists
                if (partitionWhere != null)
                {
                    if (where != null)
                    {
                        where = Expression.And(partitionWhere, where);
                    }
                    else
                    {
                        where = partitionWhere;
                    }
                }

                DataServiceQuery<U> dataServiceQuery = (DataServiceQuery<U>)context.CreateQuery<U>(innerEntitySetName).Where<U>(BinaryToLambda<U>(where));
                ContinuationToken continuationToken = null;

                InnerQuery innerQuery = new InnerQuery();
                innerQuery.Delegate = queryExecutor.ExecuteAll;
                innerQuery.Result = innerQuery.Delegate.BeginInvoke(dataServiceQuery, ref continuationToken, null, null);

                return innerQuery;
            }

            private BinaryExpression BuildInnerWhere(Expression prop, object val, out int paramCount, ref BinaryExpression partitionWhere)
            {
                BinaryExpression retExpr = null;
                paramCount = 0;

                // If the expression is an anonymous type then it likely contains multiple properties for comparison
                // We treat each comparison as an AND
                if (prop.GetType() == typeof(NewExpression))
                {
                    NewExpression newExp = (NewExpression)prop;
                    object[] values;

                    if (val.GetType() == typeof(NewExpression))
                    {
                        // If the value is an expression then it has not been evaluated
                        // However, there may be constant properties that should be included
                        NewExpression valExp = (NewExpression)val;
                        values = valExp.Arguments.ToArray();
                    }
                    else
                    {
                        values = (from p in val.GetType().GetProperties()
                                  select p.GetValue(val, null)).ToArray();
                    }

                    MemberExpression argExp;
                    object argVal;
                    BinaryExpression binExp;

                    for (int i = 0; i < newExp.Arguments.Count; i++)
                    {
                        if (newExp.Arguments[i].NodeType == ExpressionType.MemberAccess)
                        {
                            argExp = (MemberExpression)newExp.Arguments[i];
                            argVal = values[i];
                            binExp = this.GetExpressionFromValue(argExp, argVal);

                            // Partition keys cannot be included more than once, as this will cause
                            // a table scan to occur.
                            if (argExp.Member.Name != "PartitionKey")
                            {
                                if (binExp != null)
                                {
                                    if (retExpr == null)
                                    {
                                        retExpr = binExp;
                                    }
                                    else
                                    {
                                        retExpr = Expression.And(retExpr, binExp);
                                    }

                                    paramCount++;
                                }
                            }
                            else
                            {
                                // Set the partition where if it has not already been set
                                if (partitionWhere == null)
                                {
                                    partitionWhere = binExp;

                                    // Ensure the partition key is included in the parameter count
                                    // We add 2 because both the partition key AND plus the overall OR count
                                    // as 2 comparision operations.
                                    paramCount = paramCount + 2;
                                }
                            }
                        }
                    }

                }
                else
                {
                    // Only one property for comparison
                    retExpr = this.GetExpressionFromValue(prop, val);
                    paramCount++;
                }

                return retExpr;
            }

            private BinaryExpression GetExpressionFromValue(Expression exp, object value)
            {
                BinaryExpression retExpr = null;

                if (value != null)
                {
                    Type valType = value.GetType();

                    // Return null if the value is a member expression, becuase this means its
                    // value has not been evaluated
                    if (valType != typeof(MemberExpression))
                    {
                        if (valType == typeof(MethodCallExpression))
                        {
                            MethodCallExpression methodExp = (MethodCallExpression)value;

                            // Try executing the method call
                            IExecuteMethodCall executeMethodCall = (IExecuteMethodCall)Activator.CreateInstance(typeof(ExecuteMethodCall<>).MakeGenericType(methodExp.Type));

                            if (executeMethodCall.TryExecute(methodExp, out value))
                            {
                                value = Expression.Constant(value);
                            }
                            else
                            {
                                return null;
                            }
                        }
                        else if (!valType.IsSubclassOf(typeof(Expression)))
                        {
                            // Assume it's a primitive type, so make into a constant expression
                            value = Expression.Constant(value);
                        }

                        retExpr = Expression.Equal(exp, (Expression)value);
                    }
                }

                return retExpr;
            }

            private Expression<Func<T, bool>> BinaryToLambda<T>(BinaryExpression be)
            {
                var o = Expression.Parameter(typeof(T), "t");
                return Expression.Lambda<Func<T, bool>>(be, o);
            }

            private class InnerQuery
            {
                public QueryExecutorDelegate Delegate { get; set; }
                public IAsyncResult Result { get; set; } 
            }
        }

        private interface IExecuteMethodCall
        {
            bool TryExecute(MethodCallExpression methodCall, out object result);
        }

        private class ExecuteMethodCall<T> : IExecuteMethodCall
        {
            public bool TryExecute(MethodCallExpression methodCall, out object result)
            {
                bool success = false;

                try
                {
                    LambdaExpression lambdaExp = Expression.Lambda(methodCall);
                    Func<T> compiled = (Func<T>)lambdaExp.Compile();
                    result = compiled();
                    success = true;
                }
                catch (InvalidOperationException)
                {
                    // An InvalidOperationException will be thrown if the lambda expression contains
                    // any parameters, which we are unable to use.
                    result = null;
                }

                return success;
            }
        }

        private interface IJoinData
        {
            IQueryable Join(string joinType, IQueryable outerData, IQueryable innerData, Expression outerKey, Expression innerKey, Expression resultSelector, MethodCallExpression selectMany);
        }

        private class JoinData<T, U, V, W> : IJoinData
        {
            public IQueryable Join(string joinType, IQueryable outerData, IQueryable innerData, Expression outerKey, Expression innerKey, Expression resultSelector, MethodCallExpression selectMany)
            {
                var outerDataGeneric = (IQueryable<T>)outerData;
                var innerDataGeneric = (IQueryable<U>)innerData;
                var outerKeyGeneric = (Expression<Func<T, V>>)outerKey;
                var innerKeyGeneric = (Expression<Func<U, V>>)innerKey;

                switch (joinType)
                {
                    case "GroupJoin": // (outer join)
                        var groupJoinResultSelector = (Expression<Func<T, IEnumerable<U>, W>>)resultSelector;
                        IQueryable<W> data = outerDataGeneric.GroupJoin(innerDataGeneric, outerKeyGeneric, innerKeyGeneric, groupJoinResultSelector);

                        // Apply SelectMany
                        UnaryExpression ue = (UnaryExpression)selectMany.Arguments[1];
                        Expression expr = (Expression)ue.Operand;
                        Type sourceType = expr.Type.GetGenericArguments()[0];

                        UnaryExpression ue2 = (UnaryExpression)selectMany.Arguments[2];
                        Expression expr2 = (Expression)ue2.Operand;
                        Type collectionType = expr2.Type.GetGenericArguments()[1];

                        UnaryExpression ue3 = (UnaryExpression)selectMany.Arguments[2];
                        Expression expr3 = (Expression)ue3.Operand;
                        Type returnType = expr3.Type.GetGenericArguments()[2];

                        ISelectManyData seletManyData = (ISelectManyData)Activator.CreateInstance(typeof(SelectManyData<,,>).MakeGenericType
                            (sourceType, collectionType, returnType));

                        return seletManyData.SelectMany(selectMany, data);

                    default: // Join method (inner join)
                        var joinResultSelector = (Expression<Func<T, U, W>>)resultSelector;
                        return outerDataGeneric.Join(innerDataGeneric, outerKeyGeneric, innerKeyGeneric, joinResultSelector);
                }
            }
        }

        private interface ISelectManyData
        {
            IQueryable SelectMany(MethodCallExpression m, IQueryable data);
        }

        private class SelectManyData<T, U, V> : ISelectManyData
        {
            public IQueryable SelectMany(MethodCallExpression m, IQueryable data)
            {
                IQueryable<T> dataGeneric = (IQueryable<T>)data;

                UnaryExpression smCollectionSelectorEx = (UnaryExpression)m.Arguments[1];
                UnaryExpression smResultSelectorEx = (UnaryExpression)m.Arguments[2];

                var smCollectionSelector = (Expression<Func<T, IEnumerable<U>>>)smCollectionSelectorEx.Operand;
                var smResultSelector = (Expression<Func<T, U, V>>)smResultSelectorEx.Operand;

                // Perform select many
                return dataGeneric.SelectMany(smCollectionSelector, smResultSelector);
            }
        }

        private class JoinItem
        {
            public MethodCallExpression JoinExpression { get; set; }
            public MethodCallExpression SelectManyExpression { get; set; }
        }

        private class JoinBatchItem
        {
            public MethodCallExpression JoinExpression { get; set; }
            public MethodCallExpression SelectManyExpression { get; set; }
            public Type OuterType { get; set; }
            public Type InnerType { get; set; }
            public Expression OuterKey { get; set; }
            public Expression InnerKey { get; set; }
            public Expression ResultSelector { get; set; }
            public Type OuterKeyType { get; set; }
            public Type InnerKeyType { get; set; }
            public Type ResultType { get; set; }
            public string OuterEntitySet { get; set; }
            public string InnerEntitySet { get; set; }
            public GetOuterDataDelegate OuterJoinDelegate { get; set; }
            public IAsyncResult OuterAsyncResult { get; set; }
            public GetAllInnerDataDelegate InnerJoinDelegate { get; set; }
            public GetInnerDataFromUniqueDelegate InnerJoinUniqueDelegate { get; set; }
            public IAsyncResult InnerAsyncResult { get; set; }
            public IQueryable InnerData { get; set; }
        }
    }
}